import React, {createContext, useRef, useState, useEffect, forwardRef, useCallback} from 'react';
import Indicator from './indicator';
import Measurer from './measurer';
import useScroll from './useScroll';
import Menu from '../Menu';

import styles from './styles.module.css';

const INDICATOR_BAR_HEIGHT = 60;
const INITIAL_ITEM_HEIGHT = 30;

export const ScrollViewContext = createContext({
  correct: () => {},
  refreshItem:()=>{},
  enableScroll:() => {},
  disableScroll:() => {},
  setEditable:() => {},
  scrollToItem: () => {},
  setColumnWidth: () => {},
});

const findNearest = (min, max, test) =>{
  while (min <= max) {
    const mid = min + Math.floor((max - min) / 2);
    const res = test(mid);
    
    if (res === 0) {
      return mid;
    } else if (res > 0) {
      min = mid + 1;
    } else if (res < 0) {
      max = mid - 1;
    } else {
      throw Error('findNearest: invalid return');
    }
  }

  return (min > 0) ? min - 1 : 0;
};

export const ScrollView = forwardRef((props, scrollViewRef) => {

  const {
    state, 
    data,
    Row, 
    update,
    height:outerHeight,
    buffer=40,
    initialItemHeight=INITIAL_ITEM_HEIGHT,
    topWidget=[],
    editable,
    linkable,
  } = props;

  const itemCount = data.length;

  // _editable here is used for controlling other rows when one
  // is switched to editing mode.
  const [_editable, _setEditable] = useState(editable);
  const setEditable = useCallback((value) => {
    if (editable) {
      _setEditable(value);
    } else {
      _setEditable(false);
    }
  }, [editable])

  const {
    state: {
      offset: scrollOffset, 
      delta: scrollOffsetDelta
    },
    enableScroll,
    disableScroll,
    scrollTo,
  } = useScroll(scrollViewRef);

  /**
   * # 名词解释
   * 
   * 载入渲染 (Loading Pass of Rendering, LPR)
   * 对于宽高未知的item，必须要将item先渲染出来，搬运到document tree
   * 中，才有可能通过DOM方法测量其bounding box。对于有了正确高度和位置的
   * item，则不再进行测量及后续的校正渲染。
   * 
   * 校正渲染 (Correcting Pass of Rendering, CPR)
   * 在LPR结束之后，测量了每个item的bounding box（我们主要关心height）
   * 然后在渲染之前计算出将要被显示出来的item的实际位置，CPR才是真正呈现
   * 出我们想要看到的东西。
   * 
   * 首次渲染（Debut Rendering）
   * 指一个元素第一次被渲染，经过LPR和CPR两pass之后，有了正确的高度和位置
   * 
   * 回访渲染（Revisit Rendering）
   * 指在首次渲染之后，经过上下滚动重遇到一个元素时，因其已经有了正确的高度
   * 和位置，故只有LPR
   * 
   * LIOM: Last Index Of Measured
   * 指的是最后一个被测量高度的元素的位置。我们假定只有少数item会不频繁地
   * resize，因此大部分的元素在首次渲染后高度不会再变化。
   * 
   * LIOP: Last Index Of Positioned
   * 指的是最后一个被重新归位的元素的位置。在CPR阶段会一次性更新所有未归位的
   * item并将LIOP设置到min(LIOM, bottom)。此外，resizing也会设置LIOP。
   */

  const [, correct] = useState({});

  const dataRef = useRef({
    bounds: [],
    LIOM: -1, 
    LIOP: -1, 
    measuredHeightTotal: 0,
    itemsTop:0,
  });

  /**
   * # refreshItem 刷新某个（些）元素
   * 
   * 这是Poloscope向外暴露的唯一函数。通常一个React组件是通过改变输入（绑定）
   * 的属性来触发re-render的，但是当涉及到输入的是一个容器（数组/列表或类似概
   * 念，Poloscope就是用来干这个的），同时我们需要通过Poloscope对其进行增删
   * 或修改等in-place操作，去复制这个容器对象并不是个好主意。
   * 
   * 首先，如果这个容器是一个很大的数据结构的一部分，那么在用新对象代替原有对象
   * 时，也需要处理好和原对象的引用关系。
   * 
   * 其次，改变原始的数据对象会初始化整个Poloscope，这个操作不仅意味着要重绘
   * 所有屏幕上显示的Item，还会清除掉之前所有缓存的计算高度，因此不用profiling
   * 也知道这是十分昂贵的操作。
   * 
   * 所以，这里提供一个refresh方法，可以在Row的实现中通过useContext来使用，
   * 能够较细粒度地控制某个或某些Item的刷新。
   */

  const refreshItem = (index) => {
    dataRef.current.LIOP = index - 1;
    correct({});
  }


  const calcOverallHeight = () => {
    const { measuredHeightTotal, itemsTop, LIOP } = dataRef.current;
    return itemsTop + measuredHeightTotal + (itemCount - (LIOP > -1 ? LIOP + 1 : 0)) * initialItemHeight
  };



  const getIndicatorPosition = () => {
    const margin = 3;
    const overallHeight = calcOverallHeight();
    return margin + scrollOffset / (overallHeight - outerHeight) * (outerHeight - INDICATOR_BAR_HEIGHT - margin * 2)
  }
  
  const updateHeight = ({currentHeight, previousHeight, resized, index}) => {
    
    if (currentHeight === previousHeight) return;

    const {LIOP, LIOM} = dataRef.current;

    dataRef.current.bounds[index].height = currentHeight;
    dataRef.current.measuredHeightTotal += currentHeight - previousHeight;

    if (resized) {
      dataRef.current.LIOP = Math.min(LIOP, index - 1);
      correct({});
    } else {
      dataRef.current.LIOM = Math.max(LIOM, index);
    }

    // console.log(dataRef.current.measuredHeightTotal);
  }



  const updatePosition = (head, buffer) => {

    const {LIOP, LIOM, itemsTop, bounds} = dataRef.current;

    let curr = head;
    let cumulativeHeight = bounds[head]?.top ?? 0;

    const updateBound = (curr) => {

      if (curr > LIOM) {
        if (dataRef.current.bounds[curr] === undefined) {
          dataRef.current.bounds[curr] = {height: initialItemHeight};
          dataRef.current.measuredHeightTotal += initialItemHeight;
        }
      }

      const last = curr - 1;

      if (curr > LIOP) {

        const top    = bounds[last]?.bottom ?? itemsTop;
        const bottom = bounds[curr].height + top;

        Object.assign(dataRef.current.bounds[curr], {top, bottom});
      }

      cumulativeHeight += dataRef.current.bounds[curr].height;
    }

    while (curr < itemCount && cumulativeHeight < scrollOffset + outerHeight) {
      updateBound(curr);
      curr ++;
    }

    let tailNormal = curr;

    let tailBuffer = Math.min(itemCount, tailNormal + buffer);

    while (curr < tailBuffer) {
      updateBound(curr);
      curr ++;
    }

    dataRef.current.LIOP = Math.min(curr - 1, LIOM);

    return {
      tailNormal,
      tailBuffer,
    };
  }

  const scrollToItem = (index, alignPosition='CENTER') => {

    const {bounds, itemsTop} = dataRef.current;

    if (alignPosition === 'TOP') {
      scrollTo(bounds[index].top + itemsTop);
    } else if (alignPosition === 'CENTER') {
      const {top, bottom} = bounds[index];
      scrollTo(Math.max(top - outerHeight / 3 + (bottom - top) / 2, 0) + itemsTop);
    }
  }

  const setColumnWidth = (columnName, width) => {
    state.schema[columnName].width = width;
    state.innerWidth = state.schema
  }

  const getHeadIndex = () => {

    const {LIOM, bounds} = dataRef.current;

    const headNormal = findNearest(0, LIOM, index => scrollOffset - bounds[index].top);
    const headBuffer = Math.max(0, headNormal - (scrollOffsetDelta < 0 ? buffer : 10));

    return {
      headNormal,
      headBuffer
    }
  }

  const getTailIndex = (head)=> {

    const {tailNormal, tailBuffer} = updatePosition(head, scrollOffsetDelta > 0 ? buffer : 10);

    return {
      tailNormal,
      tailBuffer
    }
  }

  const renderItems = () => {

    const {bounds} = dataRef.current;

    if (data.length > 0){
      const {headBuffer, headNormal} = getHeadIndex();
      const {tailBuffer} = getTailIndex(headNormal);

      const items = [];
      for (let index = headBuffer; index < tailBuffer; index++) {

        const measurerProps = {
          key: index,
          currentHeight: bounds[index].height,
          updateHeight,
          index
        }

        const rowProps = {
          state,
          data,
          index,
          update,
          editable: _editable,
          linkable,
          style: {...bounds[index], position: 'absolute'}
        }

        items.push(<Measurer {...measurerProps}>
          <Row {...rowProps} />
        </Measurer>)
      }

      return items;
    } else {
      return [];
    }
  };

  useEffect(() => {
    const specs = {
      bounds: [],
      LIOM: -1, 
      LIOP: -1, 
      measuredHeightTotal: 0,
      itemsTop:0,
    };
    Object.assign(dataRef.current, specs);
    correct({});
    console.log(data, state);
  }, [data, state])

  const innerStyle = {
    position:'relative',
    height: calcOverallHeight(),
    overflowX: 'display',
  }

  const provided = {
    correct,
    refreshItem,
    enableScroll,
    disableScroll,
    setEditable,
    scrollToItem,
    setColumnWidth,
  }

  return <ScrollViewContext.Provider value={provided}>
    <div style={{position:'relative', height:outerHeight, overflow:'hidden'}}>
      <div id="scrollView" className={styles.outer} ref={scrollViewRef} {...{style: {height:outerHeight}}}>
        <div className={styles['top-widget']}>
          {topWidget}
        </div>
        <div className='inner-marker' style={innerStyle}>{renderItems()} </div>
      </div>
      <Indicator {...{
        barHeight: INDICATOR_BAR_HEIGHT,
        offset: getIndicatorPosition(),
        scrollOffsetDelta
      }}  />
    </div>
  </ScrollViewContext.Provider>
    
})